<?php
class SPARQLEndpoint extends SpecialPage {

    protected $m_sparqlendpointconfig;
    protected $m_sparqlendpoint;
    protected $m_equivalenturihandler;
    protected $m_sparqlparser;
    protected $m_store;
    protected $m_query;
    protected $m_query_parsed;
    protected $m_querytype;
    protected $m_outputtype;
    protected $m_useorigurisforquerying;
    protected $m_outputoriguris;
    protected $m_allowwrite; // Configured in HTML Form
    protected $m_haswriteaccess; // User permission
    protected $m_hasdeleteaccess; // User permission
    
    function __construct() {
        global $wgUser;

        parent::__construct( 'SPARQLEndpoint' );
        wfLoadExtensionMessages('SPARQLEndpoint');
        $this->m_sparqlendpointconfig = $this->getSPARQLEndpointConfig();
        $this->m_sparqlendpoint = ARC2::getStoreEndpoint( $this->m_sparqlendpointconfig );
        $this->m_sparqlparser = ARC2::getSPARQLPlusParser();
        $this->m_store = new RDFIOStore();
        $this->m_equivalenturihandler = New RDFIOEquivalentURIHandler();

        $userrights = $wgUser->getRights();
        # RDFIOUtils::showArray($userrights,"userrights");
        if ( in_array( 'edit', $userrights ) && in_array( 'createpage', $userrights ) ) {
            $this->m_haswriteaccess = true;
        } else {
            $this->m_haswriteaccess = false;
        } 
        if ( in_array( 'edit', $userrights ) && in_array( 'delete', $userrights ) ) {
            $this->m_hasdeleteaccess = true;
        } else {
            $this->m_hasdeleteaccess = false;
        } 
    }

    function execute() {
        global $wgRequest, $wgOut, $wgUser;

        $this->setHeaders();
        $this->m_query = $wgRequest->getText( 'query' );
        $this->m_allowwrite = $wgRequest->getBool( 'allowwrite', false );
        $this->m_useorigurisforquerying = $wgRequest->getBool( 'origuri_q', false );
        $this->m_outputoriguris = $wgRequest->getBool( 'origuri_o', false );
        $this->m_sparqlparser->parse( $this->m_query, '' );
        $this->m_query_parsed = $this->m_sparqlparser->getQueryInfos();
        $this->m_querytype = $this->m_query_parsed['query']['type'];
        $this->m_outputtype = $wgRequest->getText( 'output' );
        
        if ( $this->m_query == '' ) {
            $wgOut->addHTML( $this->getHTMLForm() );
        } else {
            if (!$this->m_sparqlendpoint->isSetUp()) {
                $this->m_sparqlendpoint->setUp(); /* create MySQL tables */
            }
            if ( $this->m_useorigurisforquerying ) {
                $this->convertOrigURIsToInternalURIsInQuery();
            }
            if ( $this->m_querytype == 'insert' ) {
                if ( $this->m_allowwrite ) {
                    if ( !$wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
                        die( 'Cross-site request forgery detected!' );
                    } else {
                        if ( $this->m_haswriteaccess ) {
                            $this->importTriplesInQuery();
                        } else {
                            $errortitle = "Permission error";
                            $errormessage = "The current user lacks access either to edit or create pages (or both) in this wiki.";
                            $wgOut->addHTML( RDFIOUtils::formatErrorHTML( $errortitle, $errormessage ) );
                        }
                    }
                } else {
                    die( 'Writing to wiki is not allowed' );
                }
            } else if ( $this->m_querytype == 'delete' ) { 
                if ( $this->m_allowwrite ) {
                    if ( !$wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
                        die( 'Cross-site request forgery detected!' );
                    } else {
                        if ( $this->m_hasdeleteaccess ) {
                            $this->deleteTriplesInQuery();
                        } else {
                            $errortitle = "Permission error";
                            $errormessage = "The current user lacks access either to edit or delete pages (or both) in this wiki.";
                            $wgOut->addHTML( RDFIOUtils::formatErrorHTML( $errortitle, $errormessage ) );
                        }
                    }
                } else {
                    die( 'Editing wiki is not allowed' );
                }
            } else {
                if ( $this->m_outputtype == 'htmltab' ) {
                    $wgOut->addHTML( $this->getHTMLForm( $this->m_query ) );
                } else {
                    // Disable MediaWikis theming
                    $wgOut->disable();
                    // Enables downloading as a stream, which is important for large dumps
                    wfResetOutputBuffers();
                    // Send headers telling that this is a special content type
                    // and potentially is to be downloaded as a file
                    $this->sendHeadersForOutputType( $outputtype );
                }
                if ( $wgRequest->getBool( 'showquery', false ) ) {
                    $wgOut->addHTML( "<h3>Query structure</h3><pre>" . print_r( $this->m_query_parsed, true ) . "</pre>" );
                } else {
                    if ( $this->m_outputtype != 'htmltab' ) {
                        // Make sure that ARC outputs data in a format that we can
                        // easily work with programmatically
                        $this->setOutputTypeInPost( 'php_ser' );
                    }
                    // Pass on the request handling to ARC2:s SPARQL Endpoint
                    $this->m_sparqlendpoint->handleRequest();
                    $this->handleSPARQLErrors();
                    $output = $this->m_sparqlendpoint->getResult();
                    if ( $this->m_outputoriguris ) {
                        $output = $this->convertURIsToOrigURIs( $output );
                    }
                    if ( $this->m_outputtype == 'htmltab' ) {
                        $html = RDFIOUtils::extractHTMLBodyContent( $output );
                        $html = RDFIOUtils::wikifyTables( $html );
                        $wgOut->addHTML( "<h3>Result:</h3><div style='font-size: 11px;'>" . $html . "</div>");
                    } else {
                        echo $output;
                    }
                }
            }
        }
    }

    /**
     * After a query is parsed, import the parsed data to the wiki
     */
    function importTriplesInQuery() {
        $triples = $this->m_query_parsed['query']['construct_triples'];
        $rdfImporter = new RDFIOSMWBatchWriter( $triples, 'triples_array');
        $rdfImporter->execute();
    }
    
    /**
     * After a query is parsed, delete the parsed data from the wiki
     */
    function deleteTriplesInQuery() {
        $triples = $this->m_query_parsed['query']['construct_triples'];
        $rdfImporter = new RDFIOSMWBatchWriter( $triples, 'triples_array');
        $rdfImporter->executeDelete();
    }
    
    /**
     * Die and display current errors
     */
    function handleSPARQLErrors() {
        $sparqlEndpointErrors = $this->m_sparqlendpoint->getErrors();
        if ( count( $sparqlEndpointErrors ) > 0 ) {
            $errormessage = '';
            if ( is_array( $sparqlEndpointErrors ) ) {
                foreach ( $sparqlEndpointErrors as $sparqlEndpointError ) {
                    $errormessage .= "<p>$sparqlEndpointError</p>";
                }
            } else {
                $errormessage = "<p>$sparqlEndpointErrors</p>";
            }
            die( "Error: $errormessage" );
        }
    }
    
    /**
     * For each URI in the (unparsed) query that is set by an "Original URI" property in
     * the wiki, replace it with the page's corresponding URI Resolver URI 
     */
    function convertOrigURIsToInternalURIsInQuery() {
        $origuris = RDFIOUtils::extractURIs( $this->m_query ); // TODO: Use parsed query instead
        $count = count( $origuris );
        if ( count( $origuris ) > 1 ) { // The first URI is the URI Resolver one, which always is there
                                        // TODO: Create a more robust check
            foreach ( $origuris as $origuri ) {
                $uri = $this->m_store->getURIForOrigURI( $origuri );
                if ( $uri != '' ) {
                    // Replace original uri:s into SMW:s internal URIs
                    // (The "http://.../Special:URIResolver/..." ones)
                    $query = str_replace( $origuri, $uri, $this->m_query );
                }
            }
            $this->setQuery( $query );
        }
    }
    
    /**
     * Convert all URI Resolver URIs which have a corresponding Original URI,
     * to that Original URI. 
     * @param string $output
     */
    function convertURIsToOrigURIs( $text ) {
        if ( !($this->m_outputtype == 'htmltab') ) {
            $output_structure = unserialize( $text );
            $tripleindex = $output_structure['result'];
            $ser = ARC2::getRDFXMLSerializer();
            // Serialize into RDF/XML, since it will contain
            // all URIs in un-abbreviated form, so that they 
            // can easily be replaced by search-and-replace
            $rdfxml = $ser->getSerializedIndex( $tripleindex );
            if ( $ser->getErrors() ) {
                die( "ARC Serializer Error: " . $ser->getErrors() );
            }
            $text = $rdfxml;
        }
        $uris = RDFIOUtils::extractURIs( $text );
        if ( $uris != '' ) {
            foreach ( $uris as $uri ) {
                $origuri = $this->m_store->getOrigURI( $uri );
                if ( $origuri != '' ) {
                    $text = str_replace( $uri, $origuri, $text );
                }
            }
        }
        if ( !RDFIOUtils::isURIResolverURI( $text ) ) {
            $text = RDFIOUtils::unXmlifyUris( $text );
        }
        return $text;
    }

    private function getSPARQLEndpointConfig() {
        global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $smwgARC2StoreConfig;
        $epconfig = array(
            'db_host' => $wgDBserver, /* optional, default is localhost */
            'db_name' => $wgDBname,
            'db_user' => $wgDBuser,
            'db_pwd' =>  $wgDBpassword,
            'store_name' => $smwgARC2StoreConfig['store_name'],
            'endpoint_features' => 
        array(
            'select', 
            'construct', 
            'ask', 
            'describe', 
        # 'load',
        # 'insert',
        	'delete', // TODO: Make this a user rights setting
        # 'dump' /* dump is a special command for streaming SPOG export */
        ),
            'endpoint_timeout' => 60, /* not implemented in ARC2 preview */
        # 'endpoint_read_key' => '', /* optional */
        # 'endpoint_write_key' => 'somekey', /* optional */
        # 'endpoint_max_limit' => 250, /* optional */
        );
        return $epconfig;
    }

    private function sendHeadersForOutputType( $outputtype ) {
        global $wgRequest;
        // Provide a sane filename suggestion
        $basefilename = 'SPARQLOutput_';
        switch( $outputtype )
        {
            case 'xml':
                $wgRequest->response()->header( "Content-type: application/xml; charset=utf-8" );
                $filename = urlencode( $basefilename . wfTimestampNow() . '.xml' );
                $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
                break;
            case 'rdfxml':
                $wgRequest->response()->header( "Content-type: application/xml; charset=utf-8" );
                $filename = urlencode( $basefilename . wfTimestampNow() . '.rdf.xml' );
                $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
                break;
            case 'json':
                $wgRequest->response()->header( "Content-type: text/html; charset=utf-8" );
                $filename = urlencode( $basefilename . wfTimestampNow() . '.json.txt' );
                $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
                break;
            case 'turtle':
                $wgRequest->response()->header( "Content-type: text/html; charset=utf-8" );
                $filename = urlencode( $basefilename . wfTimestampNow() . '.turtle.txt' );
                $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
                break;
            case 'htmltab':
                // For HTML table we are taking care of the output earlier
                # $wgRequest->response()->header( "Content-type: text/html; charset=utf-8" );
                # $filename = urlencode( $basefilename . wfTimestampNow() . '.html' );
                break;
            case 'tsv':
                $wgRequest->response()->header( "Content-type: text/html; charset=utf-8" );
                $filename = urlencode( $basefilename . wfTimestampNow() . '.tsv.txt' );
                $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
                break;
            default:
                $wgRequest->response()->header( "Content-type: application/xml; charset=utf-8" );
                $filename = urlencode( $basefilename . wfTimestampNow() . '.xml' );
                $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
        }

    }

    private function getHTMLForm( $query = '') {
        global $wgArticlePath, $wgUser, $wgRequest;

        $uriResolverURI = $this->m_store->getURIResolverURI();

        $defaultQuery = "@PREFIX w : <$uriResolverURI> .\n\nSELECT ?s ?p ?o\nWHERE { ?s ?p ?o }\nLIMIT 25";

        if ( $query == '' ) {
            $query = $defaultQuery;
        }

        $checked_origuri_q = $wgRequest->getBool( 'origuri_q', false ) == 1 ? ' checked="true" ' : '';
        $checked_origuri_o = $wgRequest->getBool( 'origuri_o', false ) == 1 ? ' checked="true" ' : '';
        $checked_allowwrite = $wgRequest->getBool( 'allowwrite', false ) == 1 ? ' checked="true" ' : '';
        $checked_showquery = $wgRequest->getBool( 'showquery', false ) == 1 ? ' checked="true" ' : '';

        $htmlForm = '<form method="post" action="' . str_replace('/$1', '', $wgArticlePath) . '/Special:SPARQLEndpoint"
            name="createEditQuery">
            <table border="0"><tbody>
            <tr><td colspan="2">Enter SPARQL query:</td><tr>
            <tr><td colspan="2"><textarea cols="80" rows="12" name="query">' . $query . '</textarea></td></tr>
            <tr><td width="300" style="text-align: right">Output format (if supported by query type):</td>
            <td>
            <select id="output" name="output"> 
              <!-- <option value="" >default</option> -->
              <!-- <option value="xml" >XML</option> -->
              <!-- <option value="json" >JSON</option> -->
              <!-- <option value="plain" >Plain</option> -->
              <!-- <option value="php_ser" >Serialized PHP</option> --> 
              <!-- <option value="turtle" >Turtle</option> -->
              <option value="rdfxml" >RDF/XML</option> 
              <!-- <option value="infos" >Query Structure</option> --> 
              
              <option value="htmltab" selected="selected">HTML Table</option> 
              <!-- <option value="tsv" >TSV</option> -->
            </select> 
            </td></tr>
            <tr><td width="300" style="text-align: right">Query by original URIs:</td>
            <td>
			<input type="checkbox" name="origuri_q" value="1" ' . $checked_origuri_q . '/>
            </td></tr>
            <tr><td width="300" style="text-align: right">Output original URIs:</td>
            <td>
			<input type="checkbox" name="origuri_o" value="1" ' . $checked_origuri_o . '/>
            </td></tr>
            <tr><td width="300" style="text-align: right">Allow write:</td>
            <td>
			<input type="checkbox" name="allowwrite" value="1" ' . $checked_allowwrite . '/>
            </td></tr>
            <tr><td width="300" style="text-align: right">Show query structure only:</td>
            <td>
			<input type="checkbox" name="showquery" value="1" ' . $checked_showquery . '/>
            </td></tr>
            </tbody></table>
            <input type="submit" value="Submit">' . Xml::hidden( 'token', $wgUser->editToken() ) . ' 
            </form>';
        return $htmlForm;
    }

    function getQuery() {
        return $wgRequest->getText('query');
    }

    function setQuery( $query ) {
        // Set the query in $_POST, so that ARC will grab the modified query
        $_POST['query'] = $query;
        # $this->m_query = $query;
    }

    function setOutputTypeInPost( $type ) {
        $_POST['output'] = $type;
    }
}
